# 可以自己import我们平台支持的第三方python模块，比如pandas、numpy等。
# 为了不报错引入的模块
import logging as logger
from util.util import *
from util.finance import *
import util.scheduler as scheduler
# 机器学习使用
from sklearn.preprocessing import StandardScaler
from scipy.stats.mstats import winsorize
from sklearn.linear_model import LinearRegression, Ridge

# numpy ,panda
import pandas as pd
import numpy as np 




# 4、中位数绝对偏差去极值
def median(factor):
    """3倍中位数去极值
    """
    # 求出因子值的中位数
    med = np.median(factor)
    # 求出因子值与中位数的差值，进行绝对值
    mad = np.median(abs(factor - med))

    # 定义几倍的中位数上下限
    high = med + (3 * 1.4826) * mad
    low = med - (3 * 1.4826) * mad

    # 替换上下限以外的值
    factor = np.where(factor > high, high, factor)
    factor = np.where(factor < low, low, factor)
    return factor


def stand(factor):
    """
    自实现标准化
    """
    mean = factor.mean()
    std = factor.std()
    return (factor - mean) / std


# 1、回测区间：
# 
# 2010-01-01 ~ 2018-01-01
# 2、选股：
# 
# 选股因子：6个已知方向的因子
# 选股权重：
# 因子升序从小到大分10组，第几组为所在组得分
# 因子降序从大到小分10组，第几组为所在组得分
# 数据处理：处理缺失值
# 3、调仓周期：
# 
# 调仓：每月进行一次调仓
# 交易规则：卖出已持有的股票
# 买入新的股票池当中的股票

# 在这个方法中编写任何的初始化逻辑。context对象将会在你的算法策略的任何方法之间做传递。
def init(context):
    context.stocknum = 20
    context.group_number = 10
    # 调仓函数
    scheduler.run_monthly(select_stock, tradingday=1)

 


def select_stock(context, bar_dict):
    """
    调仓函数
    :param context: 
    :param bar_dict: 
    :return: 
    """
    # 从市值中选择值小的股票
    # 1、获取选股的因子数据
    q = query(fundamentals.eod_derivative_indicator.market_cap,
              fundamentals.eod_derivative_indicator.pe_ratio,
              fundamentals.eod_derivative_indicator.pb_ratio,
              fundamentals.financial_indicator.return_on_invested_capital,
              fundamentals.financial_indicator.inc_revenue,
              fundamentals.financial_indicator.inc_profit_before_tax
              )
    factor = get_fundamentals(q).T

    # 2、定义函数去处理数据
    logger.info(factor.head())

    # 去掉了null
    factor_data = factor.dropna()

    get_scores(context, factor_data)

    # 
    balance(context, bar_dict)
    pass


def get_scores(context, factor_data):
    """
    对因子选股数据打分
    因子升序：市值、市盈率、市净率
    因子降序：ROIC、inc_revenue营业总收入和inc_profit_before_tax利润增长率
    :param context: 
    :param factor_data: 
    :return: 
    """
    # 需要对因子升序和因子降序进行区分处理
    # 因子升序 ['pe_ratio', 'pb_ratio', 'market_cap']
    columns_list = factor_data.columns
    for factor_name in columns_list:

        if factor_name in ['pe_ratio', 'pb_ratio', 'market_cap']:
            # 升序,越小分数越高
            factor_df = factor_data.sort_values(by=factor_name)[factor_name]
            pass
        else:
            #  降序,越大越高
            factor_df = factor_data.sort_values(by=factor_name, ascending=False)[factor_name]
            pass
        split = len(factor_data) // 10
        
        factor_df = pd.DataFrame(factor_df)
        factor_df[factor_name + "score"] = 0
        for i in range(context.group_number):
            if i == 9:
                factor_df[factor_name + 'score'][i * split:] = i + 1
                pass
            # else:
            factor_df[factor_name + 'score'][i * split:(i + 1) * split] = i + 1
        pass

        # 打印分数,factor_data,
        logger.info(factor_df.head())

        # 拼接到 factor_data中去
        factor_data = pd.concat([factor_data, factor_df[factor_name + "score"]], axis=1)
        pass
    logger.info(factor_data.head())

    # 求出总分
    # 先取出这几列分数
    # 求和分数
    sum_score = factor_data[
        ["market_capscore", "pe_ratioscore", "pb_ratioscore", 
         "return_on_invested_capitalscore", "inc_revenuescore",
         "inc_profit_before_taxscore"]].sum(1).sort_values()
    context.stock_list = sum_score[:context.stocknum].index
    pass


def balance(context, bar_dict):
    # 调仓频率：一个月，买卖判断
    # 进行每日调仓（多因子选股调仓周期频率会小一些）
    holding_list = context.portfolio.positions.keys()
    will_buy_list = context.stock_list.tolist()
    sell_list = set(holding_list) - set(will_buy_list)
    buy_list = set(will_buy_list) - set(holding_list)

    # 在这里才能进行卖出
    for stock in sell_list:
        if context.portfolio.positions[stock].quantity == 0:
            # 如果旧的持有的股票不在新的股票池当中，卖出
            order_target_percent(stock, 0)

    # 等比例资金买入，投资组合总价值的百分比平分10份
    weight = 1.0 / len(context.stock_list)

    # 买入最新的每日更新的股票池当中的股票
    for stock in buy_list:
        order_target_percent(stock, weight)
    # logger.info(context.portfolio.positions.keys())
    pass


# before_trading此函数会在每天策略交易开始前被调用，当天只会被调用一次
def before_trading(context):
    pass


# 你选择的证券的数据更新将会触发此段逻辑，例如日或分钟历史数据切片或者是实时数据切片更新
def handle_bar(context, bar_dict):
    pass


# after_trading函数会在每天交易结束后被调用，当天只会被调用一次
def after_trading(context):
    pass
